﻿//======================================================================
//
//	Cross-Platform XNA Menu System
//  http://cpxms.codeplex.com/
//
//  All code and content licensed under Microsoft Public License (Ms-PL)	
//
//======================================================================
//
//	File:		MenuEditorView.cs
//
//	Author:		MJP
//
//	Date:		04/07/09
//
//======================================================================

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.Diagnostics;
using System.Xml.Serialization;
using System.IO;
using System.Reflection;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Threading;

using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;

using CPXMenu;

using CPXMenuEditor.Forms;

using CPXMenuItem = CPXMenu.MenuItem;
using XNAPoint = Microsoft.Xna.Framework.Point;

namespace CPXMenuEditor.Controls
{


    public class MenuEditorView : Control
    {
        private MainFrm mainForm;
        private StatusStrip statusStrip;
        private ItemProperties itemProperties;
        private MenuTree menuTree;
        private HScrollBar hScrollBar;
        private VScrollBar vScrollBar;
        private ActionManager actionManager;
        private MenuProject project;
        private MenuProjectHost projectHost;
        private Stopwatch timer = new Stopwatch();
        private float currentTime = 0;

        private bool moving = false;
        private bool sizing = false;
        private bool canSize = false;
        private SizingMode sizingMode;
        private XNAPoint mouseStartPoint;
        private Dictionary<CPXMenuItem, XNAPoint> itemStartPoints = new Dictionary<CPXMenuItem, XNAPoint>();
        private Dictionary<CPXMenuItem, XNAPoint> itemStartDimensions = new Dictionary<CPXMenuItem, XNAPoint>();


        public void Initialize(MainFrm mainForm)
        {
            this.mainForm = mainForm;
            statusStrip = mainForm.StatusStrip;
            menuTree = mainForm.MenuTree;
            itemProperties = mainForm.ItemProperties;
            hScrollBar = mainForm.HScrollBar;
            vScrollBar = mainForm.VScrollBar;
            actionManager = mainForm.ActionManager;
            projectHost = actionManager.ProjectHost;            

            hScrollBar.ValueChanged += new EventHandler(hScrollBar_ValueChanged);
            vScrollBar.ValueChanged += new EventHandler(vScrollBar_ValueChanged);

            SetMenuProject(projectHost.MenuProject);

            Application.Idle += new EventHandler(Application_Idle);

            SetZoom(1.0f);

			actionManager.ProjectChanged += new ProjectChangedDelegate(actionManager_ProjectChanged);

            // MenuProjectHost uses the XNA input stuff, so make sure that
            // the mouse input is relative to this control.
            Microsoft.Xna.Framework.Input.Mouse.WindowHandle = this.Handle;

            timer.Start();
        }		
        

        public void SetZoom(float zoom)
        {
            if (projectHost == null)
                return;

            float scrollX = (hScrollBar.Value - hScrollBar.Minimum) / (hScrollBar.Maximum * 2.0f);
            if (hScrollBar.Maximum == 0)
                scrollX = 0;
            float scrollY = (vScrollBar.Value - vScrollBar.Minimum) / (vScrollBar.Maximum * 2.0f);
            if (vScrollBar.Maximum == 0)
                scrollY = 0;

            projectHost.Zoom = zoom;
            SetScrollBars();

            if (hScrollBar.Enabled && scrollX != 0)
                hScrollBar.Value = (int)MathHelper.Lerp(hScrollBar.Minimum, hScrollBar.Maximum, scrollX);
            if (vScrollBar.Enabled && scrollY != 0)
                vScrollBar.Value = (int)MathHelper.Lerp(vScrollBar.Minimum, vScrollBar.Maximum, scrollY);

            mainForm.StatusStrip.Items[0].Text = "Zoom: " + projectHost.Zoom.ToString("P");
        }        


        protected override void OnSizeChanged(EventArgs e)
        {
            if (project != null)
                project_DimensionsChanged(project);
        }

        protected override void OnMouseWheel(MouseEventArgs e)
        {
            base.OnMouseWheel(e);

			if (projectHost == null || projectHost.MenuProject == null)
				return;

            double logZoom = Math.Log10(projectHost.Zoom);
            logZoom += e.Delta / 3600.0f;
            SetZoom((float)Math.Pow(10, logZoom));
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);

            this.Focus();

            if (projectHost == null || projectHost.MenuProject == null)
                return;

            CPXMenuItem item = projectHost.ClickTest(new XNAPoint(e.X, e.Y));
            if (item != null)
            {
                actionManager.SelectItem(item, MultiSelect());

                if (e.Button == MouseButtons.Right)
                    mainForm.GetContextMenuForItem(item).Show(this, new System.Drawing.Point(e.X, e.Y));
            }
            else if (!MultiSelect())
                actionManager.ClearSelectedItems();

            if (e.Button == MouseButtons.Left)
            {
                if (canSize)
                    BeginSizing(e.X, e.Y);
                else
                    BeginMove(e.X, e.Y);
            }
            
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);

            if (projectHost == null || projectHost.MenuProject == null)
                return;

            if (e.Button == MouseButtons.Left)
            {
                if (sizing)
                    ContinueSizing(e.X, e.Y);
                else if (moving)
                    ContinueMove(e.X, e.Y);
            }
            else
            {
                projectHost.HighlightedItem = projectHost.ClickTest(new XNAPoint(e.X, e.Y));

                if (projectHost.HighlightedItem != null)
                {
                    const int SizingTolerance = 4;
                    bool top = false;
                    bool bottom = false;
                    bool left = false;
                    bool right = false;
                    XNAPoint position = projectHost.HighlightedItem.AbsolutePosition;
                    XNAPoint dimensions = projectHost.HighlightedItem.Dimensions;
                    XNAPoint scaledMousePos = projectHost.GetScaledMousePos(new XNAPoint(e.X, e.Y));

                    if (Math.Abs(scaledMousePos.X - position.X) <= SizingTolerance)
                        left = true;
                    else if (Math.Abs(scaledMousePos.X - (position.X + dimensions.X)) <= SizingTolerance)
                        right = true;
                    if (Math.Abs(scaledMousePos.Y - position.Y) <= SizingTolerance)
                        top = true;
                    else if (Math.Abs(scaledMousePos.Y - (position.Y + dimensions.Y)) <= SizingTolerance)
                        bottom = true;

                    canSize = left || right || top || bottom;
                    if (canSize)
                    {
                        if (top)
                        {
                            if (left)
                                sizingMode = SizingMode.TopLeft;
                            else if (right)
                                sizingMode = SizingMode.TopRight;
                            else
                                sizingMode = SizingMode.Top;
                        }
                        else if (bottom)
                        {
                            if (left)
                                sizingMode = SizingMode.BottomLeft;
                            else if (right)
                                sizingMode = SizingMode.BottomRight;
                            else
                                sizingMode = SizingMode.Bottom;
                        }
                        else if (right)
                            sizingMode = SizingMode.Right;
                        else
                            sizingMode = SizingMode.Left;


                        if (sizingMode == SizingMode.Left || sizingMode == SizingMode.Right)
                            this.Cursor = Cursors.SizeWE;
                        else if (sizingMode == SizingMode.Top || sizingMode == SizingMode.Bottom)
                            this.Cursor = Cursors.SizeNS;
                        else if (sizingMode == SizingMode.TopLeft || sizingMode == SizingMode.BottomRight)
                            this.Cursor = Cursors.SizeNWSE;
                        else if (sizingMode == SizingMode.TopRight || sizingMode == SizingMode.BottomLeft)
                            this.Cursor = Cursors.SizeNESW;
                    }
                    else
                        this.Cursor = Cursors.Default;
                }
            }
        }

        protected override void OnMouseUp(MouseEventArgs e)
        {
            base.OnMouseUp(e);

			if (projectHost == null || projectHost.MenuProject == null)
				return;

            if (e.Button == MouseButtons.Left)
            {
                if (sizing)
                    EndSizing(e.X, e.Y);
                else if (moving)
                    EndMove(e.X, e.Y);
            }
        }

        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);

			if (projectHost == null || projectHost.MenuProject == null)
				return;

            if (e.KeyCode == Keys.Delete)
                actionManager.RemoveSelectedItems();
        }

        protected override void OnDragEnter(DragEventArgs drgevent)
        {
            base.OnDragEnter(drgevent);

			if (projectHost == null || projectHost.MenuProject == null)
				return;

            // Check that it's a MenuItem type
            if (project != null)
            {
                if (drgevent.Data.GetDataPresent(typeof(string)))
                {
                    string typeName = (string)drgevent.Data.GetData(typeof(string));
                    if (CPXMenuItem.IsMenuItemType(typeName))
                    {
                        if (project.CurrentPage != null || typeName == typeof(Page).FullName)
                            drgevent.Effect = DragDropEffects.Move;
                    }
                }
            }
        }        

        protected override void OnDragDrop(DragEventArgs drgevent)
        {
            base.OnDragDrop(drgevent);

            if (project != null)
            {
                if (drgevent.Data.GetDataPresent(typeof(string)))
                {
                    string typeName = (string)drgevent.Data.GetData(typeof(string));
                    if (CPXMenuItem.IsMenuItemType(typeName))
                    {
                        Assembly assembly = Assembly.GetAssembly(typeof(CPXMenuItem));
                        Type type = assembly.GetType(typeName);
                        if (CPXMenuItem.IsMenuItemType(type))
                        {
                            System.Drawing.Point clientPoint = this.PointToClient(new System.Drawing.Point(drgevent.X, drgevent.Y));
                            XNAPoint mousePoint = new XNAPoint(clientPoint.X, clientPoint.Y);

                            CPXMenuItem parent = null;
                            if (type == typeof(Page))
                                parent = project;
                            else
                                parent = projectHost.ClickTest(mousePoint);
                            if (parent != null)
                            {
                                if (CPXMenuItem.IsValidParentChildCombination(parent.GetType(), type))
                                {
									CPXMenuItem newItem = actionManager.AddNewItem(type, parent);
                                    if (newItem is Page)
                                        project.CurrentPage = (Page)newItem;
                                    else
                                        newItem.AbsolutePosition = projectHost.GetScaledMousePos(mousePoint);
                                }
                                else
                                    MessageBox.Show(this, "The selected item type can't be added to the item it was dragged to", MainFrm.AppName, MessageBoxButtons.OK, MessageBoxIcon.Information);
                            }
                            else                            
                                return;                            
                        }
                    }
                }
            }
        }

        protected override void OnPaintBackground(PaintEventArgs pevent)
        {
            
        }

        /// <summary>
        /// Redraws the control in response to a WinForms paint message.
        /// </summary>
        protected override void OnPaint(PaintEventArgs e)
        {
            if (e.ClipRectangle.IsEmpty)
            {
                Thread.Sleep(50);
                return;
            }

            if (DesignMode)
            {
                PaintUsingSystemDrawing(e.Graphics, this.GetType().Name);
                return;
            }

            if (projectHost == null)
                return;

            // If the control was resized, reset the GraphicsDevice
            if (this.ClientSize.Width != projectHost.GraphicsDeviceService.BackBufferWidth
                || this.ClientSize.Height != projectHost.GraphicsDeviceService.BackBufferHeight)            
                projectHost.GraphicsDeviceService.SetBackBufferDimensions(ClientSize.Width, ClientSize.Height);
                                           
            float newTime = timer.ElapsedMilliseconds / 1000.0f;
            float dt = MathHelper.Clamp(newTime - currentTime, 0, 0.1f);
            currentTime = newTime;

            // Update
            projectHost.Update(dt);

            // Start up rendering
            if (projectHost.GraphicsDeviceService.BeginDraw() == GraphicsDeviceStatus.Normal)
            {
                projectHost.Render(dt);
                projectHost.GraphicsDeviceService.EndDraw();
            }
            else
                PaintUsingSystemDrawing(e.Graphics, "GraphicsDevice not available");
        }

        /// <summary>
        /// If we do not have a valid graphics device (for instance if the device
        /// is lost, or if we are running inside the Form designer), we must use
        /// regular System.Drawing method to display a status message.
        /// </summary>
        private void PaintUsingSystemDrawing(System.Drawing.Graphics graphics, string text)
        {
            graphics.Clear(System.Drawing.Color.CornflowerBlue);

            using (Brush brush = new SolidBrush(System.Drawing.Color.Black))
            {
                using (StringFormat format = new StringFormat())
                {
                    format.Alignment = StringAlignment.Center;
                    format.LineAlignment = StringAlignment.Center;

                    graphics.DrawString(text, Font, brush, ClientRectangle, format);
                }
            }
        }

        private void BeginMove(int mouseX, int mouseY)
        {
            Debug.Assert(!moving);

            itemStartPoints.Clear();
            moving = true;

            mouseStartPoint = projectHost.GetScaledMousePos(new XNAPoint(mouseX, mouseY));
            ReadOnlyCollection<CPXMenuItem> selectedItems = actionManager.SelectedItems;
            foreach (CPXMenuItem item in selectedItems)
                if (item.IsMovable)
                    itemStartPoints.Add(item, item.AbsolutePosition);            
        }

        private void ContinueMove(int mouseX, int mouseY)
        {
            Debug.Assert(moving);

            XNAPoint mousePoint = projectHost.GetScaledMousePos(new XNAPoint(mouseX, mouseY));
            XNAPoint mouseDelta = CPXMenuItem.SubtractPoint(mousePoint, mouseStartPoint);
            foreach(CPXMenuItem key in itemStartPoints.Keys)            
                key.AbsolutePosition = CPXMenuItem.AddPoint(itemStartPoints[key], mouseDelta);                
        }

        private void EndMove(int mouseX, int mouseY)
        {
            Debug.Assert(moving);

            XNAPoint mousePoint = projectHost.GetScaledMousePos(new XNAPoint(mouseX, mouseY));
            XNAPoint mouseDelta = CPXMenuItem.SubtractPoint(mousePoint, mouseStartPoint);
            object[] oldValues = new object[itemStartPoints.Count];
            object[] items = new object[itemStartPoints.Count];
            int count = 0;
            foreach (CPXMenuItem key in itemStartPoints.Keys)
            {
                key.AbsolutePosition = CPXMenuItem.AddPoint(itemStartPoints[key], mouseDelta);
                oldValues[count] = itemStartPoints[key];
                items[count] = key;
                count++;
            }

            actionManager.PropertyValueChanged(items, "AbsolutePosition", oldValues);

            moving = false;
        }

        private void BeginSizing(int mouseX, int mouseY)
        {
            Debug.Assert(!sizing);

            itemStartPoints.Clear();
            itemStartDimensions.Clear();
            sizing = true;

            mouseStartPoint = projectHost.GetScaledMousePos(new XNAPoint(mouseX, mouseY));
            ReadOnlyCollection<CPXMenuItem> selectedItems = actionManager.SelectedItems;
            foreach (CPXMenuItem item in selectedItems)
            {
                if (item.IsSizeable)
                {
                    itemStartPoints.Add(item, item.AbsolutePosition);
                    itemStartDimensions.Add(item, item.Dimensions);
                }
            }
        }

        private void ContinueSizing(int mouseX, int mouseY)
        {
            Debug.Assert(sizing);

            XNAPoint mousePoint = projectHost.GetScaledMousePos(new XNAPoint(mouseX, mouseY));
            XNAPoint mouseDelta = CPXMenuItem.SubtractPoint(mousePoint, mouseStartPoint);
            foreach (CPXMenuItem key in itemStartPoints.Keys)
            {
                XNAPoint newDimensions = itemStartDimensions[key];
                XNAPoint newPosition = itemStartPoints[key];

                if (sizingMode == SizingMode.TopRight || sizingMode == SizingMode.Right || sizingMode == SizingMode.BottomRight)
                {
                    int change = mouseDelta.X;
                    if (change < -(newDimensions.X - 1))
                        change = Min(Math.Abs(mouseDelta.X), (newDimensions.X - 1)) * Math.Sign(mouseDelta.X);
                    newDimensions.X += change;
                }
                else if (sizingMode == SizingMode.TopLeft || sizingMode == SizingMode.Left || sizingMode == SizingMode.BottomLeft)
                {
                    int change = mouseDelta.X;
                    if (change > (newDimensions.X - 1))
                        change = Min(Math.Abs(mouseDelta.X), (newDimensions.X - 1)) * Math.Sign(mouseDelta.X);
                    newDimensions.X -= change;
                    newPosition.X += change;
                }

                if (sizingMode == SizingMode.BottomLeft || sizingMode == SizingMode.Bottom || sizingMode == SizingMode.BottomRight)
                {
                    int change = mouseDelta.Y;
                    if (change < -(newDimensions.Y - 1))
                        change = Min(Math.Abs(mouseDelta.Y), (newDimensions.Y - 1)) * Math.Sign(mouseDelta.Y);
                    newDimensions.Y += change;
                }
                else if (sizingMode == SizingMode.TopLeft || sizingMode == SizingMode.Top || sizingMode == SizingMode.TopRight)
                {
                    int change = mouseDelta.Y;
                    if (change > (newDimensions.Y - 1))
                        change = Min(Math.Abs(mouseDelta.Y), (newDimensions.Y - 1)) * Math.Sign(mouseDelta.Y);
                    newDimensions.Y -= change;
                    newPosition.Y += change;
                }

                key.AbsolutePosition = newPosition;
                key.Dimensions = newDimensions;
            }
        }

        private void EndSizing(int mouseX, int mouseY)
        {
            Debug.Assert(sizing);

            XNAPoint mousePoint = projectHost.GetScaledMousePos(new XNAPoint(mouseX, mouseY));
            XNAPoint mouseDelta = CPXMenuItem.SubtractPoint(mousePoint, mouseStartPoint);
            object[] oldPositions = new object[itemStartPoints.Count];
            object[] oldDimensions = new object[itemStartDimensions.Count];
            object[] items = new object[itemStartPoints.Count];
            int count = 0;
            foreach (CPXMenuItem key in itemStartPoints.Keys)
            {
                XNAPoint newDimensions = itemStartDimensions[key];
                XNAPoint newPosition = itemStartPoints[key];

                if (sizingMode == SizingMode.TopRight || sizingMode == SizingMode.Right || sizingMode == SizingMode.BottomRight)
                {
                    int change = mouseDelta.X;
                    if (change < -(newDimensions.X - 1))
                        change = Min(Math.Abs(mouseDelta.X), (newDimensions.X - 1)) * Math.Sign(mouseDelta.X);
                    newDimensions.X += change;
                }
                else if (sizingMode == SizingMode.TopLeft || sizingMode == SizingMode.Left || sizingMode == SizingMode.BottomLeft)
                {
                    int change = mouseDelta.X;
                    if (change > (newDimensions.X - 1))
                        change = Min(Math.Abs(mouseDelta.X), (newDimensions.X - 1)) * Math.Sign(mouseDelta.X);
                    newDimensions.X -= change;
                    newPosition.X += change;
                }

                if (sizingMode == SizingMode.BottomLeft || sizingMode == SizingMode.Bottom || sizingMode == SizingMode.BottomRight)
                {
                    int change = mouseDelta.Y;
                    if (change < -(newDimensions.Y - 1))
                        change = Min(Math.Abs(mouseDelta.Y), (newDimensions.Y - 1)) * Math.Sign(mouseDelta.Y);
                    newDimensions.Y += change;
                }
                else if (sizingMode == SizingMode.TopLeft || sizingMode == SizingMode.Top || sizingMode == SizingMode.TopRight)
                {
                    int change = mouseDelta.Y;
                    if (change > (newDimensions.Y - 1))
                        change = Min(Math.Abs(mouseDelta.Y), (newDimensions.Y - 1)) * Math.Sign(mouseDelta.Y);
                    newDimensions.Y -= change;
                    newPosition.Y += change;
                }

                key.AbsolutePosition = newPosition;
                key.Dimensions = newDimensions;

                oldPositions[count] = itemStartPoints[key];
                oldDimensions[count] = itemStartDimensions[key];
                items[count] = key;
                count++;
            }

            actionManager.BeginCompoundAction();
            actionManager.PropertyValueChanged(items, "AbsolutePosition", oldPositions);
            actionManager.PropertyValueChanged(items, "Dimensions", oldDimensions);
            actionManager.EndCompoundAction();

            sizing = false;
        }

        private bool MultiSelect()
        {
            return Microsoft.Xna.Framework.Input.Keyboard.GetState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.LeftControl);
        }

        private void SetMenuProject(MenuProject project)
        {
            if (project != null)
            {
                project.DimensionsChanged -= project_DimensionsChanged;
            }

            this.project = project;
			projectHost.HighlightedItem = null;

            if (project != null)
            {
                project.DimensionsChanged += project_DimensionsChanged;
            }

			SetZoom(1.0f);
            SetScrollBars(); 
        }

        private void SetScrollBars()
        {
			if (project == null)
			{
				hScrollBar.Enabled = false;
				vScrollBar.Enabled = false;
				return;
			}

            const int buffer = 100;

            int totalWidth = (int)(project.Dimensions.X * projectHost.Zoom + buffer * 2);
            int totalHeight = (int)(project.Dimensions.Y * projectHost.Zoom + buffer * 2);

            if (totalWidth <= this.Width)
            {
                hScrollBar.Value = 0;
                hScrollBar.Minimum = 0;
                hScrollBar.Maximum = 0;
                hScrollBar.Enabled = false;
            }
            else
            {
                hScrollBar.Enabled = true;

                int left = (int)(-buffer - (project.Dimensions.X * projectHost.Zoom) / 2.0f);
                int right = (int)(buffer + (project.Dimensions.X * projectHost.Zoom) / 2.0f);

                hScrollBar.Minimum =  left + (int)(this.Width / 2.0f);
                hScrollBar.Maximum = right - (int)(this.Width / 2.0f);
            }

            if (totalHeight <= this.Height)
            {
                vScrollBar.Value = 0;
                vScrollBar.Minimum = 0;
                vScrollBar.Maximum = 0;
                vScrollBar.Enabled = false;
            }
            else
            {                
                vScrollBar.Enabled = true;

                int top = (int)(-buffer - (project.Dimensions.Y * projectHost.Zoom) / 2.0f);
                int bottom = (int)(buffer + (project.Dimensions.Y * projectHost.Zoom) / 2.0f);

                vScrollBar.Minimum = top + (int)(this.Height / 2.0f);
                vScrollBar.Maximum = bottom - (int)(this.Height / 2.0f);
            }       
        }        

        void project_DimensionsChanged(CPXMenuItem item)
        {
            SetScrollBars();      
        }

        void vScrollBar_ValueChanged(object sender, EventArgs e)
        {
            if (projectHost != null)
            {
                projectHost.DrawOffsetY = -vScrollBar.Value;
                this.Invalidate();
            }
        }

        void hScrollBar_ValueChanged(object sender, EventArgs e)
        {
            if (projectHost != null)
            {
                projectHost.DrawOffsetX = -hScrollBar.Value;
                this.Invalidate();
            }
        }

        void Application_Idle(object sender, EventArgs e)
        {
            //if (actionManager != null)
            //{
            //    if (actionManager.PreviewMode && this.ClientRectangle.Contains(this.PointToClient(System.Windows.Forms.Cursor.Position)))
            //        System.Windows.Forms.Cursor.Hide();
            //    else
            //        System.Windows.Forms.Cursor.Show();
            //}

            this.Invalidate();
        }

		void actionManager_ProjectChanged(ActionManager manager, MenuProject oldProject, MenuProject newProject)
		{
			SetMenuProject(newProject);
		}

        public static int Min(int a, int b)
        {
            if (a < b)
                return a;
            else
                return b;
        }

        public static int Max(int a, int b)
        {
            if (a > b)
                return a;
            else
                return b;
        }

        private enum SizingMode
        {
            Top = 0,
            TopRight = 1,
            Right = 2,
            BottomRight = 3,
            Bottom = 4,
            BottomLeft = 5,
            Left = 6,
            TopLeft = 7
        }
    }
}